package org.springframework.samples.mvc.basic.controller;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

import org.springframework.http.HttpStatus;
import org.springframework.samples.mvc.basic.business.BusinessService;
import org.springframework.samples.mvc.basic.business.ExcelService;
import org.springframework.samples.mvc.basic.business.ExcelServiceReflection;
import org.springframework.samples.mvc.basic.exception.ResourceNotFoundException;
import org.springframework.samples.mvc.basic.model.Identifiable;
import org.springframework.samples.mvc.basic.model.Message;
import org.springframework.samples.mvc.basic.model.ReflectionModel;
import org.springframework.samples.mvc.basic.model.UIFormModel;
import org.springframework.samples.mvc.basic.pagination.Pagination;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

public abstract class CRUDController<T extends Identifiable> {

	private static final String ERRORS = "errors";
	protected static final int PAGE_SIZE = 20;
	private static final String UI_MODEL = "UIModel";
	private static final String OBJECTS = "objects";
	private static final String TABS = "tabs";
	private static final String PAGINATION = "pagination";
    private static final String QUERY_STRING = "queryString";

    private BusinessService<T> service;

    public CRUDController(BusinessService<T> service) {
        this.service = service;
    }

    protected abstract Class<T> getPojoClass();

    protected abstract T createNewObject();

    protected abstract String getLowerCapitalizedClassName();

    protected abstract UIFormModel[] getDetailModels();
    
    protected BusinessService<T> getBusinessService() {
    	return service;
    }

    @RequestMapping(method = RequestMethod.GET)
    public String create(Model model) {
        model.addAttribute(createNewObject());
        UIFormModel uiModel = getReflectionModel();
        model.addAttribute(UI_MODEL, uiModel);
        return getLowerCapitalizedClassName() + "/createForm";
    }

    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    public String edit(Model model, @RequestParam Integer id) {
        UIFormModel uiModel = getReflectionModel();
        model.addAttribute(UI_MODEL, uiModel);
        model.addAttribute("id", id);
        return getLowerCapitalizedClassName() + "/editForm";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String create(@Valid T object, BindingResult result, Model model) {
    	UIFormModel uiModel = getReflectionModel();
        model.addAttribute(UI_MODEL, uiModel);
        if (result.hasErrors()) {
        	List<FieldError> errors = result.getFieldErrors();
        	addJson(model, ERRORS, errors);
            return getLowerCapitalizedClassName() + "/createForm";
        }
        service.save(object);
        model.addAttribute(new Message("INFO", "Object with ID: "
                + object.getId()));
        return "redirect:/" + getLowerCapitalizedClassName() + "/list";
    }

    @RequestMapping(value = "{id}", method = RequestMethod.GET)
    public String preview(@PathVariable Long id, Model model) {
    	UIFormModel uiModel = getReflectionModel();
        model.addAttribute(UI_MODEL, uiModel);
        T object = service.findById(id);
        if (object == null) {
            throw new ResourceNotFoundException(id);
        }
        model.addAttribute("object", object);
        return getLowerCapitalizedClassName() + "/viewForm";
    }
    
    @RequestMapping(value = "/list", method = RequestMethod.GET)
    public String list(Model model, @RequestParam(defaultValue = "1") Integer pg) {
    	Map<String, Object> modelMap = new HashMap<String, Object>();
    	putModel(modelMap);
    	addPagination(modelMap, pg);
        model.addAllAttributes(modelMap);
        return getLowerCapitalizedClassName() + "/listAll";
    }
    
    @RequestMapping(value = {"/table"}, method = RequestMethod.GET, produces="application/json")
    public @ResponseBody Map<String, Object> table(@RequestParam(defaultValue = "1") Integer pg) {
        Map<String, Object> model = new HashMap<String, Object>();
        Pagination pagination = addPagination(model, pg);
		fillListModel(model, pagination.getStartIndex());
        return model;
    }

    @RequestMapping(value = {"/tabbed"}, method = RequestMethod.GET, produces="application/json")
    public @ResponseBody Map<String, Object> tabbed(@RequestParam(defaultValue = "1") Integer pg) {
    	Map<String, Object> model = new HashMap<String, Object>();
    	Pagination pagination = addPagination(model, pg);
    	fillListModel(model, pagination.getStartIndex());
        model.put(TABS, getDetailModels());
        return model;
    }
    
    @RequestMapping(value = "/getAll", method = RequestMethod.GET, produces="application/json")
    public @ResponseBody Map<String, Object> getAll(@RequestParam(value="ob", defaultValue="id") String orderBy) {
    	Map<String, Object> model = new HashMap<String, Object>();
    	fillListModel(model, orderBy);
        return model;
    }
    
	@RequestMapping(value = {"/query/list"}, method = RequestMethod.GET)
    public String queryList(HttpServletRequest request, Model model, @RequestParam(defaultValue = "1") Integer pg, T queryInfo) {
    	Map<String, Object> modelMap = new HashMap<String, Object>();
    	model.addAttribute(QUERY_STRING, request.getQueryString());
    	addJson(model, "queryObject", queryInfo);
        model.addAllAttributes(modelMap);
        return getLowerCapitalizedClassName() + "/queryList";
    }
    
    @RequestMapping(value = {"/query/tabbed"}, method = RequestMethod.GET, produces="application/json")
    public @ResponseBody Map<String, Object> queryTabbed(@RequestParam(defaultValue = "1") Integer pg, T queryInfo) {
    	Map<String, Object> model = new HashMap<String, Object>();
    	fillListModel(model, pg, queryInfo);
        model.put(TABS, getDetailModels());
        return model;
    }

    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    @ResponseStatus(value = HttpStatus.OK)
    public void delete(Long id) {
        service.delete(id);
//        return "redirect:/" + getLowerCapitalizedClassName() + "/list";
    }

    @RequestMapping(value = "/download", method = RequestMethod.GET)
    public void download(HttpServletResponse response) throws IOException {
        ExcelService<T> excelService = new ExcelServiceReflection<T>(getPojoClass());
        response.setHeader("Content-disposition",
                "attachment; filename="+getLowerCapitalizedClassName()+".xls");
        excelService.exportToExcel(service.findAll()).write(response.getOutputStream());
    }
    
    protected ReflectionModel<T> getReflectionModel() {
		return new ReflectionModel<T>(getPojoClass(), "id");
	}
    
    private void fillListModel(Map<String, Object> model, int pg, T queryInfo) {
    	Pagination pagination = addPagination(model, pg);
        List<T> objects = service.findAll(pagination.getStartIndex(), PAGE_SIZE, queryInfo);
        putModel(model);
        putObjectsInMap(model, objects);
    }

    private void fillListModel(Map<String, Object> model, int startIndex) {
        List<T> objects = service.findAll(startIndex, PAGE_SIZE);
        putModel(model);
        putObjectsInMap(model, objects);
    }
    
    private void fillListModel(Map<String, Object> model, String orderBy) {
    	List<T> objects = service.findAll(orderBy);
        putModel(model);
        putObjectsInMap(model, objects);
	}

	protected Pagination addPagination(Map<String, Object> model, int pg) {
		return addPagination(model, pg, service.count());
	}
	
	protected Pagination addPagination(Map<String, Object> model, int pg, int total) {
		Pagination pagination = new Pagination(total, PAGE_SIZE);
		pagination.setCurrentPage(pg);
		model.put(PAGINATION, pagination);
		return pagination;
	}

    protected void putObjectsInMap(Map<String, Object> model, List<T> objects) {
        if (objects.size() == 0) {
            model.put("message", new Message(Message.INFO, "No Data found"));
        }
        model.put(OBJECTS, objects);
    }
    
    protected void putModel(Map<String, Object> model) {
    	UIFormModel uiModel = getReflectionModel();
        model.put(UI_MODEL, uiModel);
    }
    
    private void addJson(Model model, String key, Object errors) {
    	String errs = JsonConverter.convertToString(errors);
    	System.out.println("Validation Errors: " + errs);
		model.addAttribute(key, errs);
	}

}
